package com.allwees.core.common.parser.el;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@NoArgsConstructor
public class ExpressionParser implements BeanFactoryAware,  Serializable {

    private static final String RESULT_VARIABLE = "result";
    protected final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<>(64);
    protected final SpelExpressionParser elParser = new SpelExpressionParser();
    protected final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    protected BeanFactory beanFactory;

    public Object parse(String expression, ExpressionInteractiveObject interactiveObject) {

        Class<?> targetClass = getTargetClass(interactiveObject.getTarget());
        ParseExpressionRootObject rootObject = interactiveObject.rootObject();
        AnnotatedElementKey methodKey = getMethodKey(targetClass, interactiveObject.getMethod());
        EvaluationContext evaluationContext = createEvaluationContext(rootObject, interactiveObject.getResult());

        return getExpression(methodKey, expression).getValue(evaluationContext);
    }

    protected Expression getExpression(AnnotatedElementKey elementKey, String expression) {

        ExpressionKey expressionKey = createKey(elementKey, expression);
        Expression expr = keyCache.get(expressionKey);
        if (expr == null) {
            expr = getElParser().parseExpression(expression);
            keyCache.put(expressionKey, expr);
        }
        return expr;
    }

    protected SpelExpressionParser getElParser() {
        return elParser;
    }

    protected ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) {
        return new ExpressionKey(elementKey, expression);
    }

    protected EvaluationContext createEvaluationContext(ParseExpressionRootObject rootObject, Object result) {

        ParseEvaluationContext evaluationContext = new ParseEvaluationContext(
                rootObject, rootObject.getMethod(), rootObject.getArgs(), getParameterNameDiscoverer());

        if (result != null) {
            evaluationContext.setVariable(RESULT_VARIABLE, result);
        }

        if (beanFactory != null) {
            evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
        }
        return evaluationContext;
    }

    protected ParameterNameDiscoverer getParameterNameDiscoverer() {
        return this.parameterNameDiscoverer;
    }

    protected Class<?> getTargetClass(Object target) {
        return AopProxyUtils.ultimateTargetClass(target);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    private AnnotatedElementKey getMethodKey(Class targetClass, Method method) {
        Method targetMethod = (!Proxy.isProxyClass(targetClass) ?
                AopUtils.getMostSpecificMethod(method, targetClass) : method);
        return new AnnotatedElementKey(targetMethod, targetClass);
    }

    protected static class ExpressionKey implements Comparable<ExpressionKey> {

        private final AnnotatedElementKey element;

        private final String expression;

        protected ExpressionKey(AnnotatedElementKey element, String expression) {
            Assert.notNull(element, "AnnotatedElementKey must not be null");
            Assert.notNull(expression, "Expression must not be null");
            this.element = element;
            this.expression = expression;
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof ExpressionKey)) {
                return false;
            }
            ExpressionKey otherKey = (ExpressionKey) other;
            return (this.element.equals(otherKey.element) &&
                    ObjectUtils.nullSafeEquals(this.expression, otherKey.expression));
        }

        @Override
        public int hashCode() {
            return this.element.hashCode() * 29 + this.expression.hashCode();
        }

        @Override
        public String toString() {
            return this.element + " with expression \"" + this.expression + "\"";
        }

        @Override
        public int compareTo(ExpressionKey other) {
            int result = this.element.toString().compareTo(other.element.toString());
            if (result == 0) {
                result = this.expression.compareTo(other.expression);
            }
            return result;
        }
    }
}

